package cn.sciento.starter.keyencrypt.core;

import cn.sciento.core.exception.CommonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import cn.sciento.core.util.EncryptionUtils;
import cn.sciento.core.util.TokenUtils;
import cn.sciento.starter.keyencrypt.util.EncryptUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public class EncryptionService implements IEncryptionService {
  private static final Logger logger = LoggerFactory.getLogger(EncryptionService.class);
  
  private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
  
  private static final String SEPARATOR = ":";
  
  private static final String TOKEN_SKIP = "Bearer".toLowerCase() + " ";
  
  private static final int PLAIN_PART_COUNT = 3;
  
  private static final int ACCESS_TOKEN_START = 0;
  
  private static final int ACCESS_TOKEN_END = 10;
  
  private final EncryptProperties encryptProperties;
  
  private ObjectMapper mapper;
  
  private String cipherPrefix = "=";
  
  private String cipherSuffix = "=";
  
  public EncryptionService(EncryptProperties encryptProperties) {
    this.encryptProperties = encryptProperties;
    if (StringUtils.hasText(encryptProperties.getCipherFormat())) {
      String[] split = encryptProperties.getCipherFormat().split("\\%s");
      if (!encryptProperties.getCipherFormat().contains("%s") || split.length > 2 || split.length <= 0)
        throw new IllegalArgumentException("The cipher text format is illegal : " + encryptProperties.getCipherFormat()); 
      this.cipherPrefix = split[0];
      if (split.length == 2) {
        this.cipherSuffix = split[1];
      } else {
        this.cipherPrefix = "";
      } 
    } 
  }
  
  public String decrypt(String value, String tableName, String accessToken, boolean ignoreUserConflict) {
    if (StringUtils.isEmpty(value) || "null".equals(value))
      return value; 
    Assert.notNull(tableName, "decrypt key can not be null!");
    Assert.isTrue(isCipher(value), "Is not a cipher : " + value);
    if (StringUtils.isEmpty(value))
      return value; 
    String decrypt = EncryptionUtils.AES.decryptWithUrlDecoder(unwrap(value), this.encryptProperties.getSecretKey());
    List<String> decryptKeyList = Arrays.asList(StringUtils.delimitedListToStringArray(decrypt, ":"));
    if (decryptKeyList.size() != 3)
      handleCheckError(value); 
    String encryptTableName = decryptKeyList.get(1);
    if (!checkEncryptKey(tableName, encryptTableName))
      handleCheckError(value); 
    if (!ignoreUserConflict) {
      String encryptToken = decryptKeyList.get(2);
      if (!checkToken(encryptToken, accessToken))
        handleCheckError(value); 
    } 
    return decryptKeyList.get(0);
  }
  
  public String encrypt(String id, String tableName) {
    return encrypt(id, tableName, TokenUtils.getToken());
  }
  
  public String encrypt(String id, String tableName, String accessToken) {
    if (!EncryptContext.isAllowedEncrypt() || !EncryptContext.isEncrypt())
      return id; 
    List<String> encryptKeyList = new ArrayList<>(8);
    encryptKeyList.add(id);
    encryptKeyList.add(tableName);
    encryptKeyList.add(getSubToken(accessToken));
    return wrap(EncryptionUtils.AES.encryptWithUrlEncoder(StringUtils.collectionToDelimitedString(encryptKeyList, ":"), this.encryptProperties.getSecretKey()));
  }
  
  public Object decrypt(JsonParser parser, JsonDeserializer<?> deserializer, DeserializationContext context, JavaType javaType, Encrypt encrypt) {
    try {
      if (javaType.isCollectionLikeType() || javaType.isArrayType()) {
        ArrayNode node = (ArrayNode)parser.getCodec().readTree(parser);
        List<Object> objectList = new ArrayList(node.size());
        for (JsonNode jsonNode : node) {
          if (EncryptUtils.ignoreValue(encrypt, jsonNode.textValue())) {
            objectList.add(jsonNode.textValue());
            continue;
          } 
          if (BeanUtils.isSimpleValueType(javaType.getContentType().getRawClass())) {
            objectList.add(decrypt(jsonNode.textValue(), encrypt.value(), encrypt.ignoreUserConflict()));
            continue;
          } 
          logger.error("@Encrypt annotation is only allowed to add to simple attributes(including its array or collection), current : {} > {}", javaType.getRawClass(), javaType.getContentType().getRawClass());
          objectList.add(this.mapper.readValue(jsonNode.traverse(), javaType.getContentType().getRawClass()));
        } 
        return this.mapper.readValue(this.mapper.writeValueAsString(objectList), javaType);
      } 
      if (javaType.isMapLikeType() && javaType.getKeyType().isFinal()) {
        ObjectNode mapValue = (ObjectNode)parser.getCodec().readTree(parser);
        Map<Object, Object> value = new HashMap<>(mapValue.size());
        Iterator<String> stringIterator = mapValue.fieldNames();
        while (stringIterator.hasNext()) {
          String key = stringIterator.next();
          JsonNode valueNode = mapValue.get(key);
          if (!EncryptUtils.ignoreValue(encrypt, key) && isCipher(key))
            key = decrypt(key, encrypt.value(), encrypt.ignoreUserConflict()); 
          if (valueNode.isTextual() && !EncryptUtils.ignoreValue(encrypt, valueNode.textValue()) && isCipher(valueNode.textValue())) {
            value.put(key, decrypt(valueNode.textValue(), encrypt.value(), encrypt.ignoreUserConflict()));
            continue;
          } 
          value.put(key, valueNode);
        } 
        return this.mapper.readValue(this.mapper.writeValueAsString(value), javaType);
      } 
      if (StringUtils.isEmpty(parser.getValueAsString()))
        return null; 
      String decrypt = parser.getValueAsString();
      if (!EncryptUtils.ignoreValue(encrypt, decrypt))
        decrypt = decrypt(parser.getValueAsString(), encrypt.value(), encrypt.ignoreUserConflict()); 
      if (deserializer != null)
        return deserializer.deserialize(this.mapper.getFactory()
            .createParser(decrypt.getBytes(DEFAULT_CHARSET)), context); 
      return this.mapper.readValue(decrypt, javaType);
    } catch (Exception e) {
      throw new CommonException("Unable to decrypt document", e);
    } 
  }
  
  private String wrap(String content) {
    return this.cipherPrefix + content + this.cipherSuffix;
  }
  
  private String unwrap(String content) {
    return content.substring(this.cipherPrefix.length(), content.length() - this.cipherSuffix.length());
  }
  
  public boolean isCipher(String content) {
    return (content.startsWith(this.cipherPrefix) && content.endsWith(this.cipherSuffix));
  }
  
  public void setObjectMapper(ObjectMapper objectMapper) {
    this.mapper = objectMapper;
  }
  
  protected boolean checkEncryptKey(String tableName, String encryptTableName) {
    if (tableName.equalsIgnoreCase(encryptTableName) || "8ac987c65ffa4974bfd9460058468706"
      .equals(encryptTableName))
      return true; 
    if ("8ac987c65ffa4974bfd9460058468706".equals(tableName))
      return true; 
    return false;
  }
  
  protected boolean checkToken(String encryptToken, String accessToken) {
    String token = getSubToken(accessToken);
    return token.equals(encryptToken);
  }
  
  protected String getSubToken(String token) {
    if (StringUtils.hasText(token)) {
      if (token.toLowerCase().startsWith(TOKEN_SKIP))
        token = token.substring(TOKEN_SKIP.length()); 
      token = org.apache.commons.lang3.StringUtils.substring(token.replace("-", ""), 0, 10);
    } else {
      token = "NO_TOKEN";
    } 
    return token;
  }
  
  protected void handleCheckError(String value) {
    throw new CommonException("decrypt error ! decrypt key = " + value);
  }
}
